package me.zhanghai.ohos.materialratingbar;

import me.zhanghai.harmmony.materialratingbar.ResourceTable;
import me.zhanghai.ohos.materialratingbar.vector.VectorDrawableCompat;
import ohos.agp.components.*;
import ohos.agp.components.element.Element;
import ohos.agp.render.BlendMode;
import ohos.agp.render.Canvas;
import ohos.agp.render.ColorFilter;
import ohos.agp.utils.Color;
import ohos.agp.utils.Rect;
import ohos.agp.utils.RectFloat;
import ohos.app.Context;
import ohos.multimodalinput.event.TouchEvent;

public class MaterialRatingBar extends ComponentContainer implements Component.TouchEventListener {

    private static final int DEFAULT_RATING_ITEM = 5;
    private static final int DEFAULT_MAX = 10;
    private static final int DEFAULT_MIN = 0;
    private static final float DEFAULT_GRAIN_SIZE = 0.5f;

    private Element mFilledElement;
    private Element mHalfFilledElement;
    private Element mThumbElement;
    private Element mUnfilledElement;

    private ColorFilter mColorFilter;

    private float mRootAlpha = 1f;

    private int mRatingItem = DEFAULT_RATING_ITEM;
    private int mMax = DEFAULT_MAX;
    private int mMin = DEFAULT_MIN;
    private float mGrainSize = DEFAULT_GRAIN_SIZE;
    private float mScore = 0;
    private int progress = 0;
    private int color = 0xFF2289A4;

    private boolean isOperable = true;


    private RatingChangedListener mRatingChangedListener;

    public MaterialRatingBar(Context context) {
        this(context, null);
    }

    public MaterialRatingBar(Context context, AttrSet attrSet) {
        this(context, attrSet, null);
    }

    public MaterialRatingBar(Context context, AttrSet attrSet, String styleName) {
        super(context, attrSet, styleName);

        init(attrSet);
    }

    private void init(AttrSet attrSet) {
        if (attrSet != null) {
            attrSet.getAttr("mrb_color").ifPresent(attr -> color = attr.getColorValue().getValue());
            attrSet.getAttr("mrb_score").ifPresent(attr -> mScore = attr.getFloatValue());
            attrSet.getAttr("mrb_isOperable").ifPresent(attr -> isOperable = attr.getBoolValue());
            attrSet.getAttr("mrb_min").ifPresent(attr -> mMin = attr.getIntegerValue());
            attrSet.getAttr("mrb_max").ifPresent(attr -> mMax = attr.getIntegerValue());
            attrSet.getAttr("mrb_ratingItem").ifPresent(attr -> mRatingItem = attr.getIntegerValue());
            attrSet.getAttr("mrb_rootAlpha").ifPresent(attr -> mRootAlpha = attr.getFloatValue());
            attrSet.getAttr("mrb_grainSize").ifPresent(attr -> mGrainSize = attr.getFloatValue());
            attrSet.getAttr("mrb_progress").ifPresent(attr -> progress = attr.getIntegerValue());

            attrSet.getAttr("mrb_filledElement").ifPresent(attr -> mFilledElement = attr.getElement());
            attrSet.getAttr("mrb_halfFilledElement").ifPresent(attr -> mHalfFilledElement = attr.getElement());
            attrSet.getAttr("mrb_thumbElement").ifPresent(attr -> mThumbElement = attr.getElement());
            attrSet.getAttr("mrb_unfilledElement").ifPresent(attr -> mUnfilledElement = attr.getElement());
        }
        if (mGrainSize >= 0) {
            setGrainSize(mGrainSize);
        } else {
            setGrainSize(0.5f);
        }
        if (mScore >= 0) {
            setScore(mScore);
        }
        if (mFilledElement == null) {
            mFilledElement = VectorDrawableCompat.create(getContext(), ResourceTable.Graphic_mrb_star_icon_black_36dp);
            if (mFilledElement != null) {
                ((VectorDrawableCompat) mFilledElement).setComponent(this);
                if (color != 0){
                    ((VectorDrawableCompat) mFilledElement).setColorFilter(mColorFilter == null ? new ColorFilter(color, BlendMode.SRC_IN) : mColorFilter);
                }
            }
        }
        if (mUnfilledElement == null) {
            mUnfilledElement = VectorDrawableCompat.create(getContext(), ResourceTable.Graphic_mrb_star_border_icon_black_36dp);
            if (mUnfilledElement != null) {
                ((VectorDrawableCompat) mUnfilledElement).setComponent(this);
            }
        }
        if (mHalfFilledElement == null) {
            mHalfFilledElement = VectorDrawableCompat.create(getContext(), ResourceTable.Graphic_mrb_star_border_icon_black_36dp);
            if (mHalfFilledElement != null) {
                ((VectorDrawableCompat) mHalfFilledElement).setComponent(this);
                if (color != 0){
                    ((VectorDrawableCompat) mHalfFilledElement).setColorFilter(mColorFilter == null ? new ColorFilter(color, BlendMode.SRC_IN) : mColorFilter);
                }
            }
        }
        if (mThumbElement == null) {
            mThumbElement = VectorDrawableCompat.create(getContext(), ResourceTable.Graphic_mrb_star_border_icon_black_36dp);
            if (mThumbElement != null) {
                ((VectorDrawableCompat) mThumbElement).setComponent(this);
            }
        }

        setEstimateSizeListener(new EstimateSizeListener() {
            @Override
            public boolean onEstimateSize(int widthEstimateConfig, int heightEstimateConfig) {

                int height = measureDimension(mFilledElement.getHeight(), heightEstimateConfig);
                int ratio = mFilledElement.getWidth() / mFilledElement.getHeight();
                int width = Math.round(height * ratio * getRatingItem());
                int w = measureDimension(width, widthEstimateConfig);

                setEstimatedSize(w, height);


                mUnfilledElement.setBounds(new Rect(0, 0, mFilledElement.getWidth(), mFilledElement.getHeight()));
                mFilledElement.setBounds(new Rect(0, 0, mFilledElement.getWidth(), mFilledElement.getHeight()));
                mHalfFilledElement.setBounds(new Rect(0, 0, mFilledElement.getWidth(), mFilledElement.getHeight()));

                return true;
            }
        });
        addDrawTask(new DrawTask() {
            @Override
            public void onDraw(Component component, Canvas canvas) {
                draw(component, canvas);
            }
        });
        setTouchEventListener(this);

    }

    private int measureDimension(int defaultSize, int measureSpec) {
        int result = defaultSize;
        int specMode = EstimateSpec.getMode(measureSpec);
        int specSize = EstimateSpec.getSize(measureSpec);
        if (specMode == EstimateSpec.PRECISE) {
            result = specSize;
        } else if (specMode == EstimateSpec.NOT_EXCEED) {
            result = Math.min(defaultSize, specSize);
        } else {
            result = defaultSize;
        }
        return result;
    }

    private void draw(Component component, Canvas canvas){
        if (mFilledElement == null){
            return;
        }
        if (mColorFilter != null) {
            if (mFilledElement instanceof VectorDrawableCompat) {
                ((VectorDrawableCompat) mFilledElement).setColorFilter(mColorFilter);
            }
            if (mHalfFilledElement != null) {
                if (mHalfFilledElement instanceof VectorDrawableCompat) {
                    ((VectorDrawableCompat) mHalfFilledElement).setColorFilter(mColorFilter);
                }
            }
        }


        if (isLayoutRtl()) {
            canvas.translate(getWidth() - getPaddingRight(), getPaddingTop());
            canvas.scale(-1.0f, 1.0f);

        }else {
            canvas.translate(getPaddingLeft(), getPaddingTop());
        }
        if (progress >= getMax()) {
            progress = mMax;
        }
        int num = getProgress() / (getMax() / getRatingItem());
        float grain = getProgress() % (getMax() / getRatingItem());

        int itemWidth = mFilledElement.getWidth();
        int itemHeight = mFilledElement.getHeight();

        int item = (getWidth() / getRatingItem());
        int outWidth = (item - itemWidth);
        int outWidth2 = outWidth / 2;

        if (num <= 0) {
            for (int i = 0; i < getRatingItem(); i++) {
                int saveCount = canvas.save();
                if (i != 0) {
                    canvas.translate(i * item + outWidth2, 0);
                }else {
                    canvas.translate(outWidth2, 0);
                }
                mUnfilledElement.drawToCanvas(canvas);
                canvas.restoreToCount(saveCount);
            }
        } else {
            for (int i = 1; i <= getRatingItem(); i++) {
                if (num < i) {
                    canvas.save();
                    canvas.translate((i - 1) * item + outWidth2, 0);
                    mUnfilledElement.drawToCanvas(canvas);
                    canvas.restore();
                }
            }
        }
        canvas.save();
        float clipW = getWidth() * ((float)getProgress() / getMax());
        float numClipW = item * num;
        float clip = clipW - numClipW;
        float right = itemWidth * (clip / item);

        canvas.clipRect(new RectFloat(0, 0, numClipW + outWidth2 + right, itemHeight), Canvas.ClipOp.INTERSECT);
        for (int i = 0; i < getRatingItem(); i++) {
            if (i != 0){
                canvas.translate(item , 0);
            }else {
                canvas.translate(outWidth2 , 0);
            }
            mFilledElement.drawToCanvas(canvas);
        }

        canvas.restore();

        if (grain >= 1 && num < getRatingItem() && getProgress() > 0) {
            canvas.save();
            canvas.translate(num * item +  outWidth2, 0);
            mHalfFilledElement.drawToCanvas(canvas);
            canvas.restore();
        }

    }

    @Override
    public boolean onTouchEvent(Component component, TouchEvent touchEvent) {

        if (isOperable) {
            switch (touchEvent.getAction()) {
                case TouchEvent.PRIMARY_POINT_DOWN:
                case TouchEvent.HOVER_POINTER_ENTER:
                    trackTouchEvent(touchEvent);
                    if (mRatingChangedListener != null) {
                        mRatingChangedListener.onProgressChanged(MaterialRatingBar.this, (int) getProgress(), true);

                        mRatingChangedListener.onStartTrackingTouch(MaterialRatingBar.this);
                    }
                    break;
                case TouchEvent.POINT_MOVE:
                case TouchEvent.HOVER_POINTER_MOVE:
                    trackTouchEvent(touchEvent);
                    if (mRatingChangedListener != null) {
                        mRatingChangedListener.onProgressChanged(MaterialRatingBar.this, (int) getProgress(), true);
                    }
                    break;
                case TouchEvent.CANCEL:
                    if (mRatingChangedListener != null) {
                        mRatingChangedListener.onStopTrackingTouch(MaterialRatingBar.this);
                    }
                    break;
            }
        }
        return true;
    }

    public boolean isLayoutRtl() {
        return (getLayoutDirection() == LayoutDirection.RTL);
    }

    private void trackTouchEvent(TouchEvent event) {
        final int x = Math.round(event.getPointerPosition(0).getX());
        final int y = Math.round(event.getPointerPosition(0).getY());
        final int width = getWidth();
        final int availableWidth = width - getPaddingLeft() - getPaddingRight();

        final float scale;
        float progress = 0.0f;
        if (isLayoutRtl()) {
            if (x > width - getPaddingRight()) {
                scale = 0.0f;
            } else if (x < getPaddingLeft()) {
                scale = 1.0f;
            } else {
                scale = (availableWidth - x + getPaddingLeft()) / (float) availableWidth;
                progress = 0.6f;
            }
        }else {
            if (x < getPaddingLeft()) {
                scale = 0.0f;
            } else if (x > width - getPaddingRight()) {
                scale = 1.0f;
            } else {
                scale = (x - getPaddingLeft()) / (float) availableWidth;
                progress = 0.6f;
            }
        }
        final int range = getMax() - getMin();
        progress += scale * range + getMin();

        setProgress(Math.round(progress));
        invalidate();
    }

    private float getRatingScale() {
        int min = getMin();
        int max = getMax();
        int range = max - min;
        return range > 0 ? (getProgress() - min) / (float) range : 0;
    }

    /**
     * progress that fits into a star
     * @return The amount of progress that fits into a star
     */
    private float getProgressPerStar() {
        if (mRatingItem > 0) {
            return 1f * getMax() / mRatingItem;
        } else {
            return 1;
        }
    }

    public void drawBackground(Canvas canvas) {
        if (mUnfilledElement != null) {
            mUnfilledElement.drawToCanvas(canvas);

        }
    }

    public void setColorFiler(ColorFilter colorFilter) {
        mColorFilter = colorFilter;
    }

    public void setRootAlpha(float alpha) {
        this.mRootAlpha = alpha;
    }

    public float getRootAlpha() {
        return mRootAlpha;
    }

    public void setFilledElement(Element filledElement) {
        this.mFilledElement = filledElement;
        if (mFilledElement instanceof VectorDrawableCompat) {
            ((VectorDrawableCompat) mFilledElement).setComponent(this);
        }
        invalidate();
    }

    public void setHalfFilledElement(Element halfFilledElement) {
        this.mHalfFilledElement = halfFilledElement;
        if (mHalfFilledElement instanceof VectorDrawableCompat) {
            ((VectorDrawableCompat) mHalfFilledElement).setComponent(this);
        }
        invalidate();
    }

    public void setThumbElement(Element thumbElement) {
        this.mThumbElement = thumbElement;
        invalidate();
    }

    public void setUnfilledElement(Element unfilledElement) {
        this.mUnfilledElement = unfilledElement;
        if (mUnfilledElement instanceof VectorDrawableCompat) {
            ((VectorDrawableCompat) mUnfilledElement).setComponent(this);
        }
        invalidate();
    }

    public void setRatingItem(int ratingItem) {
        if (ratingItem <= 0) {
            return;
        }
        this.mRatingItem = ratingItem;
    }

    public void setGrainSize(float grainSize) {
        if (grainSize <= 0) {
            return;
        }
        this.mGrainSize = grainSize;
        final float newMax = mRatingItem / grainSize;
        final int newProgress = (int) (newMax / getMax() * getProgress());
        setMax((int) newMax);
        setProgress(newProgress);
    }

    public void setProgress(int progress) {
        this.progress = Math.min(progress, mMax);

        if (mRatingChangedListener != null) {
            mRatingChangedListener.onProgressChanged(MaterialRatingBar.this, this.progress, true);
        }

    }

    public int getProgress() {
        return progress;
    }

    public void setScore(float score) {
        if (score < 0) {
            return;
        }
        setProgress(Math.round(score * getProgressPerStar()));
    }

    public boolean isOperable() {
        return isOperable;
    }

    public void setOperable(boolean operable) {
        isOperable = operable;
    }

    public void setMin(int min) {
        if (min < 0) {
            this.mMin = 0;
        } else this.mMin = Math.min(min, mMax);


    }

    public int getMin() {
        return mMin;
    }

    public void setMax(int max) {
        if (max <= 0) {
            this.mMax = 1;
        } else this.mMax = Math.max(mMin, max);

    }

    public int getMax() {
        return mMax;
    }

    public Element getFilledElement() {
        return mFilledElement;
    }

    public Element getHalfFilledElement() {
        return mHalfFilledElement;
    }

    public Element getThumbElement() {
        return mThumbElement;
    }

    public Element getUnfilledElement() {
        return mUnfilledElement;
    }


    public int getRatingItem() {
        return mRatingItem;
    }

    public float getGrainSize() {
        return (float) getRatingItem() / getMax();
    }

    public float getScore() {
        return getProgress() / getProgressPerStar();
    }

    public void setRatingChangedListener(RatingChangedListener ratingChangedListener) {
        this.mRatingChangedListener = ratingChangedListener;
    }


    public interface RatingChangedListener {
        /**
         * Notifies change events of a rating.
         * This method notifies the change of the number of rating items displayed on the rating.
         *
         * @param ratingBar Indicates the rating to listen for.
         * @param progress  Indicates the progress value to set.
         * @param fromUser  Indicates whether the change is made by users. The value is true if the change is made by users; the value is false otherwise.
         */
        void onProgressChanged(MaterialRatingBar ratingBar, int progress, boolean fromUser);

        /**
         * Notifies stop touch events of a rating.
         * This method is called to respond to the stop touch events.
         *
         * @param ratingBar Indicates the rating to listen for.
         */
        void onStartTrackingTouch(MaterialRatingBar ratingBar);

        /**
         * Notifies start touch events of a rating.
         * This method notifies start touch events of a rating. Rating will call back this method to respond to the start touch events.
         *
         * @param ratingBar Indicates the rating to listen for.
         */
        void onStopTrackingTouch(MaterialRatingBar ratingBar);
    }


}
